package com.jwiki.app.rpcui.util;

import java.beans.PropertyDescriptor;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.compress.archivers.ArchiveEntry;
import org.apache.commons.compress.archivers.ArchiveException;
import org.apache.commons.compress.archivers.ArchiveInputStream;
import org.apache.commons.compress.archivers.ArchiveStreamFactory;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

import com.jwiki.app.rpcui.attribute.FieldAttribute;
import com.jwiki.app.rpcui.attribute.ParameterAttribute;
import com.jwiki.app.rpcui.config.Configurations;
import com.jwiki.app.rpcui.util.handler.ContextHandler;

public abstract class ClassUtils {

    private static final Set<String> CLASS_TYPES = new HashSet<String>();

    private static final Set<String> LOADED_JARS = new HashSet<String>();

    private static final Map<String, Class<?>> PRIMITIVE_TYPES = new HashMap<String, Class<?>>();

    private static final Map<String, Class<?>> ADVANCED_TYPES = new HashMap<String, Class<?>>();

    private static final Map<String, String> ARRAY_TYPES = new HashMap<String, String>();

    static {
        // array class type
        ARRAY_TYPES.put(char.class.getName(), "[C");
        ARRAY_TYPES.put(short.class.getName(), "[S");
        ARRAY_TYPES.put(int.class.getName(), "[I");
        ARRAY_TYPES.put(long.class.getName(), "[J");
        ARRAY_TYPES.put(float.class.getName(), "[F");
        ARRAY_TYPES.put(double.class.getName(), "[D");
        ARRAY_TYPES.put(byte.class.getName(), "[B");
        ARRAY_TYPES.put(boolean.class.getName(), "[Z");

        // primitive class type
        PRIMITIVE_TYPES.put(char.class.getName(), char.class);
        PRIMITIVE_TYPES.put(short.class.getName(), short.class);
        PRIMITIVE_TYPES.put(int.class.getName(), int.class);
        PRIMITIVE_TYPES.put(long.class.getName(), long.class);
        PRIMITIVE_TYPES.put(float.class.getName(), float.class);
        PRIMITIVE_TYPES.put(double.class.getName(), double.class);
        PRIMITIVE_TYPES.put(byte.class.getName(), byte.class);
        PRIMITIVE_TYPES.put(boolean.class.getName(), boolean.class);
        PRIMITIVE_TYPES.put(BigDecimal.class.getName(), BigDecimal.class);

        // advanced class type
        ADVANCED_TYPES.put(char.class.getName(), Character.class);
        ADVANCED_TYPES.put(short.class.getName(), Short.class);
        ADVANCED_TYPES.put(int.class.getName(), Integer.class);
        ADVANCED_TYPES.put(long.class.getName(), Long.class);
        ADVANCED_TYPES.put(float.class.getName(), Float.class);
        ADVANCED_TYPES.put(double.class.getName(), Double.class);
        ADVANCED_TYPES.put(byte.class.getName(), Byte.class);
        ADVANCED_TYPES.put(boolean.class.getName(), Boolean.class);
        ADVANCED_TYPES.put(BigDecimal.class.getName(), BigDecimal.class);

        // class type
        CLASS_TYPES.add(Character.class.getName());
        CLASS_TYPES.add(Short.class.getName());
        CLASS_TYPES.add(Integer.class.getName());
        CLASS_TYPES.add(Long.class.getName());
        CLASS_TYPES.add(Float.class.getName());
        CLASS_TYPES.add(Double.class.getName());
        CLASS_TYPES.add(String.class.getName());
        CLASS_TYPES.add(Byte.class.getName());
        CLASS_TYPES.add(Boolean.class.getName());
        CLASS_TYPES.add(Enum.class.getName());
        CLASS_TYPES.add(Date.class.getName());
        CLASS_TYPES.add(List.class.getName());
        CLASS_TYPES.add(Map.class.getName());
        CLASS_TYPES.add(Set.class.getName());
        CLASS_TYPES.add(Collection.class.getName());
        CLASS_TYPES.addAll(PRIMITIVE_TYPES.keySet());
    }

    public static boolean isPrimitiveClassType(String className) {
        if (PRIMITIVE_TYPES.containsKey(className)) {
            return true;
        }
        return false;
    }

    public static Class<?> getPrimitiveClassType(String className) {
        for (String typeName : PRIMITIVE_TYPES.keySet()) {
            if (typeName.equals(className)) {
                return PRIMITIVE_TYPES.get(typeName);
            }
        }
        return null;
    }

    public static Class<?> getAdvancedClassType(String className) {
        for (String typeName : ADVANCED_TYPES.keySet()) {
            if (typeName.equals(className)) {
                return ADVANCED_TYPES.get(typeName);
            }
        }
        return null;
    }

    public static void loadExtendedJarFiles() throws Exception {
        // get extended jars
        Iterator<File> iterator = FileUtils.iterateFiles(new File(Configurations.DIR_WORK), new String[] { "jar" }, true);
        // check loaded jars
        Set<File> newLoadedJars = new HashSet<File>();
        while (iterator.hasNext()) {
            File jarFile = iterator.next();
            if (!LOADED_JARS.contains(jarFile.getName())) {
                newLoadedJars.add(jarFile);
                LOADED_JARS.add(jarFile.getName());
            }
        }
        // add jars
        if (newLoadedJars.size() > 0) {
            Method method = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
            boolean accessible = method.isAccessible();
            try {
                if (!accessible) {
                    method.setAccessible(true);
                }
                URLClassLoader classLoader = (URLClassLoader) ClassLoader.getSystemClassLoader();
                for (File jarFile : newLoadedJars) {
                    URL url = jarFile.toURI().toURL();
                    method.invoke(classLoader, url);
                }
            } finally {
                method.setAccessible(accessible);
            }
        }
    }

    public static String getActualTypeClassName(Type classType) {
        Type declareType = classType;
        if (classType instanceof ParameterizedType) {
            declareType = ((ParameterizedType) classType).getRawType();
        } else if (classType instanceof GenericArrayType) {
            declareType = ((GenericArrayType) classType).getGenericComponentType();
            return getActualTypeClassName(declareType) + "[]";
        }

        if (declareType instanceof Class) {
            return ((Class<?>) declareType).getName();
        }

        return Object.class.getName();
    }

    public static String toDisplayClassName(String className) {
        if (className.startsWith("[L") && className.endsWith(";")) {
            return StringUtils.join(className.substring(2, className.length() - 1), "[]");
        } else {
            for (String arrayType : ARRAY_TYPES.keySet()) {
                if (ARRAY_TYPES.get(arrayType).equals(className)) {
                    return StringUtils.join(arrayType, "[]");
                }
            }
        }
        return className;
    }

    public static String toJavaClassName(String className) {
        if (className.endsWith("[]")) {
            for (String arrayType : ARRAY_TYPES.keySet()) {
                if (className.endsWith(StringUtils.join(arrayType, "[]"))) {
                    return ARRAY_TYPES.get(arrayType);
                }
            }
            return StringUtils.join("[L", className.substring(0, className.length() - 2), ";");
        }
        return className;
    }

    public static Type[] getMethodParameterTypes(ParameterAttribute attribute) throws Exception {
        // load extended jars
        ClassUtils.loadExtendedJarFiles();
        // get parameters
        Class<?> interfaceClass = Class.forName(attribute.getInterfaceName());
        String[] parameterTypes = attribute.getParameterTypes();
        Class<?>[] parameterClasses = new Class<?>[parameterTypes.length];
        for (int i = 0; i < parameterTypes.length; i++) {
            if (ClassUtils.isPrimitiveClassType(parameterTypes[i])) {
                parameterClasses[i] = ClassUtils.getPrimitiveClassType(parameterTypes[i]);
            } else {
                parameterClasses[i] = Class.forName(toJavaClassName(parameterTypes[i]));
            }
        }
        // get target method
        Method targetMethod = interfaceClass.getMethod(attribute.getMethodName(), parameterClasses);

        return targetMethod.getGenericParameterTypes();
    }

    public static void applyAndHandleContext(ContextHandler handler) throws Exception {
        // load extended jars
        loadExtendedJarFiles();
        try {
            handler.handle();
        } finally {
            // nothing to do
        }
    }

    public static Set<FieldAttribute> getFieldAttributes(String beanName) throws Exception {
        if (CLASS_TYPES.contains(beanName)) {
            return null;
        }
        // load extended jars
        loadExtendedJarFiles();

        Set<FieldAttribute> fieldAttrs = Collections.emptySet();

        Class<?> clazz = Class.forName(toJavaClassName(beanName));
        if (!clazz.isEnum()) {
            PropertyDescriptor[] propDescs = BeanUtils.getPropertyDescriptors(clazz);
            if (ArrayUtils.isNotEmpty(propDescs)) {
                fieldAttrs = new HashSet<FieldAttribute>();

                // get attribute of field
                for (PropertyDescriptor propDesc : propDescs) {
                    if (propDesc.getWriteMethod() != null) {
                        Type[] paramTypes = propDesc.getWriteMethod().getGenericParameterTypes();
                        // add fields of bean class
                        if (ArrayUtils.isNotEmpty(paramTypes)) {
                            FieldAttribute fieldAttr = new FieldAttribute();
                            fieldAttr.setName(propDesc.getName());
                            fieldAttr.setType(paramTypes[0]);
                            fieldAttrs.add(fieldAttr);
                        }
                    }
                }
            }
        }

        return fieldAttrs;
    }

    public static List<Class<?>> getAllInterfaces(File jarFile) throws Exception {
        String directory = FilenameUtils.concat(Configurations.DIR_TEMP, FilenameUtils.getBaseName(jarFile.getName()));
        decompressJarFile(jarFile, directory);
        Iterator<File> iterator = FileUtils.iterateFiles(new File(directory), new String[] { "class" }, true);

        List<Class<?>> interfaces = new ArrayList<Class<?>>();
        try {
            // load extended jars
            loadExtendedJarFiles();

            Properties currentProperty = Configurations.getCurrentProperty();
            // include interface
            String propIncludeClass = currentProperty.getProperty("app.includeClass");
            boolean includeClassChecked = false;
            String[] includeClasses = null;
            if (StringUtils.isNotBlank(propIncludeClass)) {
                includeClassChecked = true;
                includeClasses = propIncludeClass.split(",");
            }
            // exclude interface
            String propExcludeClass = currentProperty.getProperty("app.excludeClass");
            boolean exludeClassChecked = false;
            String[] excludeClasses = null;
            if (StringUtils.isNotBlank(propExcludeClass)) {
                exludeClassChecked = true;
                excludeClasses = propExcludeClass.split(",");
            }

            // get interfaces
            while (iterator.hasNext()) {
                File classFile = iterator.next();
                String classFilePath = classFile.getPath().substring(directory.length() + 1).replace(File.separator, "/");
                if (classFilePath.contains("$")) {
                    continue;
                }

                boolean isIncluded = true;
                if (includeClassChecked) {
                    isIncluded = false;
                    for (String includeClass : includeClasses) {
                        PathMatcher pathMatcher = new AntPathMatcher();
                        if (pathMatcher.match(includeClass, classFilePath)) {
                            isIncluded = true;
                            break;
                        }
                    }
                }

                if (exludeClassChecked) {
                    for (String excludeClass : excludeClasses) {
                        PathMatcher pathMatcher = new AntPathMatcher();
                        if (pathMatcher.match(excludeClass, classFilePath)) {
                            isIncluded = false;
                            break;
                        }
                    }
                }

                if (isIncluded) {
                    String className = classFilePath.replace("/", ".").replace(".class", "");
                    try {
                        Class<?> clazz = Class.forName(className, false, ClassUtils.class.getClassLoader());
                        if (clazz.isInterface() && !(clazz.isAnnotation() || clazz.isEnum())) {
                            interfaces.add(clazz);
                        }
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        } finally {
            FileUtils.deleteDirectory(new File(directory));
        }

        return interfaces;
    }

    private static void decompressJarFile(File jarFile, String directory) throws ArchiveException, IOException {
        ArchiveInputStream input = null;
        try {
            BufferedInputStream inputBuffer = new BufferedInputStream(new FileInputStream(jarFile));
            input = new ArchiveStreamFactory().createArchiveInputStream(ArchiveStreamFactory.JAR, inputBuffer);

            ArchiveEntry entry = null;
            while ((entry = input.getNextEntry()) != null) {
                if (entry.isDirectory()) {
                    new File(directory, entry.getName()).mkdirs();
                } else {
                    OutputStream output = FileUtils.openOutputStream(new File(directory, entry.getName()));
                    IOUtils.copy(input, output);
                    output.close();
                }
            }
        } finally {
            if (input != null) {
                try {
                    input.close();
                } catch (IOException e) {
                    // ignore exception
                }
            }
        }
    }

}
